#!/usr/bin/python3
# -*- coding: utf-8 -*-

from abc import ABCMeta, abstractmethod, abstractproperty
import multiprocessing
import optparse
import os
import shutil
import subprocess
import sys
import tempfile
import threading
import time


# ------------------------------------------------------------------------------

parser = optparse.OptionParser()

parser.add_option(
  '--verbose', dest='verbose', default=False,
  action='store_true',
  help='Print verbose logging messages during the run.'
)

parser.add_option(
  '--verbose_command', dest='verbose_command', default=False,
  action='store_true',
  help='Print each command-line being run.'
)

parser.add_option(
  '--ncpu', dest='ncpu', default=None, type='int',
  help='Number of CPU to distribute the work.'
)

parser.add_option(
  '--repo_dir', dest='repo_dir', default='/Repo', type='string',
  help='Path of the installation repository.'
)

parser.add_option(
  '--source_dir', dest='source_dir', default='/Source', type='string',
  help='Path of the source repository.'
)

parser.add_option(
  '--build_dir', dest='build_dir', default='/tmp', type='string',
  help='Path of the build repository.'
)

(options, args) = parser.parse_args()

# ------------------------------------------------------------------------------

NCPU = options.ncpu or multiprocessing.cpu_count()

# ------------------------------------------------------------------------------


class Command(object):
  """Runs a command in a sub-process.

  Captures the command stdout/stderr in files.
  """

  def __init__(self, args, cwd=None, env=None):
    self._args = tuple(map(str, args))
    self._proc = None
    self._status = None
    self._out_fd = None
    self._out_name = None
    self._err_fd = None
    self._err_name = None
    self._cwd = cwd
    self._env = env

  def Run(self):
    basename = os.path.basename(self._args[0])
    gmtime = time.gmtime()
    timestamp = '%s-%s' % (''.join(map(str, gmtime[0:3])),
                           ''.join(map(str, gmtime[3:6])))
    prefix = 'subprocess.%s.%s.' % (basename, timestamp)
    self._out_fd, self._out_name = (
      tempfile.mkstemp(suffix='.stdout', prefix=prefix, dir='/tmp'))
    self._err_fd, self._err_name = (
      tempfile.mkstemp(suffix='.stderr', prefix=prefix, dir='/tmp'))

    def SetupFDs():
      os.close(0)  # no stdin
      assert os.open('/dev/null', 0) == 0

      os.close(1)  # redirect stdout to file
      assert os.open(self._out_name, os.O_WRONLY | os.O_APPEND) == 1

      os.close(2)  # redirect stderr to file
      assert os.open(self._err_name, os.O_WRONLY | os.O_APPEND) == 2

    print('Running: %s' % ' '.join(self._args))
    self._proc = subprocess.Popen(
      args=self._args,
      preexec_fn=SetupFDs,
      stdin=None,   # fd 0
      stdout=None,  # fd 1
      stderr=None,  # fd 2
      close_fds=True,
      cwd=self._cwd,
      env=self._env,
    )
    return self

  def Wait(self):
    self._status = self._proc.wait()
    return self

  def Kill(self):
    self._proc.kill()
    self._proc = None

  @property
  def status(self):
    return self._status


# ------------------------------------------------------------------------------


class Node(object):
  __metaclass__ = ABCMeta

  SOURCE_ROOT_DIR = options.source_dir
  REPOSITORY_ROOT_DIR = options.repo_dir
  BUILD_ROOT_DIR = options.build_dir

  def __init__(self):
    self.workflow = self.workflow_type(self)

  @abstractproperty
  def workflow_type(self):
    pass

  @abstractproperty
  def source_package(self):
    pass

  @property
  def source_package_strip_components(self):
    return 0  # do not strip any path from source package

  @abstractproperty
  def name(self):
    pass

  @abstractproperty
  def version(self):
    pass

  @property
  def source_dir(self):
    return os.path.join(self.SOURCE_ROOT_DIR, self.name, self.version)

  @property
  def install_dir(self):
    return os.path.join(self.REPOSITORY_ROOT_DIR, self.name, self.version)

  @property
  def build_dir(self):
    return os.path.join(self.BUILD_ROOT_DIR, self.name, self.version)


# ------------------------------------------------------------------------------


class WorkflowRunner(object):
  def __init__(self, workflow):
    self._workflow = workflow

  @property
  def workflow(self):
    return self._workflow

  def Run(self):
    for state in self.workflow.states:
      # TODO: backup state, to allow resuming
      getattr(self.workflow, state)()


class AutotoolsWorkflow(object):
  """Workflow for autotools driven packages."""

  def __init__(self, node):
    self._node = node

  @property
  def node(self):
    return self._node

  @property
  def states(self):
    return ('SetupSource', 'Configure', 'Build', 'Install', 'Clean')

  def SetupSource(self):
    if os.path.exists(self.node.source_dir):
      shutil.rmtree(self.node.source_dir)
    os.makedirs(self.node.source_dir)

    assert os.path.exists(self.node.source_package)
    cl = [
      '/bin/tar',
      '-xf', self.node.source_package,
      '-C', self.node.source_dir,
      '--strip-components', self.node.source_package_strip_components
    ]
    assert Command(cl, cwd='/tmp').Run().Wait().status == 0

  def Configure(self):
    config_script = os.path.join(self.node.source_dir, 'configure')
    assert os.path.exists(config_script)

    if not os.path.exists(self.node.build_dir):
      os.makedirs(self.node.build_dir)

    cl = [config_script, '--prefix=%s' % self.node.install_dir,]
		cl += self.node.configure_options
    assert Command(cl, cwd=self.node.build_dir).Run().Wait().status == 0

  def Build(self):
    assert os.path.exists('/usr/bin/make')
    cl = ['/usr/bin/make', '--jobs=%d' % NCPU,]
    assert Command(cl, cwd=self.node.build_dir).Run().Wait().status == 0

  def Install(self):
    assert os.path.exists('/usr/bin/make')
    cl = ['/usr/bin/make', 'install', '--jobs=%d' % NCPU,]
    assert Command(cl, cwd=self.node.build_dir).Run().Wait().status == 0

  def Clean(self):
    if os.path.exists(self.node.build_dir):
      shutil.rmtree(self.node.build_dir)
    if os.path.exists(self.node.source_dir):
      shutil.rmtree(self.node.source_dir)


# ------------------------------------------------------------------------------


class Version(object):
  def __init__(self, v):
    self._v = v


# ------------------------------------------------------------------------------


class GMP(Node):
  """GMP Multi-Precision."""

  name = 'gmp'
  version = '5.0.2'

  source_package = '/usr/portage/distfiles/gmp-5.0.2.tar.bz2'
  source_package_strip_components = 1

  workflow_type = AutotoolsWorkflow

  @property
  def build_dir(self):
    # GMP apparently requires building in the source tree.
    return self.source_dir


class MPFR(Node):
  """MPFR Multi-Precision Floating-point with exact Rounding."""

  name = 'mpfr'
  version = '3.1.0'

  source_package = '/usr/portage/distfiles/mpfr-3.1.0.tar.bz2'
  source_package_strip_components = 1

  workflow_type = AutotoolsWorkflow


class MPC(Node):
  """Multi-precision library."""

  name = 'mpc'
  version = '0.9'

  source_package = '/usr/portage/distfiles/mpc-0.9.tar.gz'
  source_package_strip_components = 1

  workflow_type = AutotoolsWorkflow


class GCC(Node):
  """Gnu compiler collection."""

  name = 'gcc'
  version = '4.6.2'

  source_package = '/usr/portage/distfiles/gcc-4.6.2.tar.bz2'
  source_package_strip_components = 1

  workflow_type = AutotoolsWorkflow

  @property
  def dependencies(self):
    return [
      GMP(),
      MPFR(),
      MPC(),
    ]

	@property
	def configure_options(self):
		return [
			'--build=x86_64-linux-gnu',
			'--enable-languages=c,c++,fortran',
			'--with-mpc=%s' % ...,
			'--with-mpfr=%s' % ...,
			'--with-gmp=%s' % ...,
		]

class Glibc(Node):
  """GNU C Library."""

  name = 'glibc'
  version = '2.14.1'

  source_package = '/usr/portage/distfiles/glibc-2.14.1.tar.bz2'
  source_package_strip_components = 1

  workflow_type = AutotoolsWorkflow

	# Requires gcc >=4.6.2 for building

	@property
	def configure_options(self):
		return [
			'--build=x86_64-linux-gnu',
		]


node = Glibc()

wfrunner = WorkflowRunner(node.workflow)
wfrunner.Run()
